package com.google.code.swingonfire.utility;

import java.awt.Color;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import com.google.code.swingonfire.utility.interpolation.Interpolation;

import static java.awt.Color.*;

/**
 * @author HAM
 */
public class Gradient
{

    public static abstract class GradientStep
    {
        private final Gradient gradient;

        protected GradientStep(Gradient gradient)
        {
            super();

            this.gradient = gradient;
        }

        protected Gradient getGradient()
        {
            return gradient;
        }
    }

    public static class GradientLength extends GradientStep
    {
        private Interpolation interpolation;
        private int stepCount;
        private double fraction;
        private int minimumLength;
        private int maximumLength;

        protected GradientLength(Gradient gradient)
        {
            super(gradient);
        }

        protected Interpolation getInterpolation()
        {
            return interpolation;
        }

        protected int getStepCount()
        {
            return stepCount;
        }

        protected double getFraction()
        {
            return fraction;
        }

        protected int getMinimumLength()
        {
            return minimumLength;
        }

        protected int getMaximumLength()
        {
            return maximumLength;
        }

        public GradientColor length(int length)
        {
            return fraction(null, 1, 0, length, length);
        }

        public GradientColor length(Interpolation interpolation, int stepCount, int length)
        {
            return fraction(interpolation, stepCount, length, length);
        }

        public GradientColor fraction(double fraction)
        {
            return fraction(null, 1, fraction, 0, Integer.MAX_VALUE);
        }

        public GradientColor fraction(Interpolation interpolation, int stepCount, double fraction)
        {
            return fraction(interpolation, stepCount, fraction, 0, Integer.MAX_VALUE);
        }

        public GradientColor fraction(double fraction, int maximumLength)
        {
            return fraction(null, 1, fraction, 0, maximumLength);
        }

        public GradientColor fraction(Interpolation interpolation, int stepCount, double fraction, int maximumLength)
        {
            return fraction(interpolation, stepCount, fraction, 0, maximumLength);
        }

        public GradientColor fraction(double fraction, int minimumLength, int maximumLength)
        {
            return fraction(null, 1, fraction, minimumLength, maximumLength);
        }

        public GradientColor fraction(Interpolation interpolation, int stepCount, double fraction, int minimumLength,
            int maximumLength)
        {
            if (stepCount < 1)
            {
                throw new IllegalArgumentException("StepCount < 1");
            }

            this.interpolation = interpolation;
            this.stepCount = stepCount;
            this.fraction = fraction;
            this.minimumLength = minimumLength;
            this.maximumLength = maximumLength;

            return getGradient().add(new GradientColor(getGradient()));
        }
    }

    /**
     * A color step
     * 
     * @author Manfred HANTSCHEL
     */
    public static class GradientColor extends GradientStep
    {
        private Color color;

        protected GradientColor(Gradient gradient)
        {
            super(gradient);
        }

        protected Color getColor()
        {
            return color;
        }

        /**
         * Sets the color
         * 
         * @param color the color
         * @return the next step
         */
        public GradientLength color(Color color)
        {
            this.color = color;

            return getGradient().add(new GradientLength(getGradient()));
        }

        /**
         * Switches the color (immediately) from the specified end color to the next color
         * 
         * @param endColor the end color
         * @param continueWithColor the next color
         * @return the next step
         */
        public GradientLength color(Color endColor, Color continueWithColor)
        {
            return color(endColor).length(0).color(continueWithColor);
        }

        /**
         * Ends the defintion of the gradient and returns it
         * 
         * @param color the end color
         * @return the gradient
         */
        public Gradient end(Color color)
        {
            this.color = color;

            return getGradient();
        }
    }

    /**
     * Creates a gradient starting the specified color
     * 
     * @param color the color
     * @return the next step
     */
    public static GradientLength start(Color color)
    {
        Gradient gradient = new Gradient();
        GradientColor start = new GradientColor(gradient);

        gradient.add(start);

        return start.color(color);
    }

    private final List<GradientStep> steps = new ArrayList<GradientStep>();

    private final LRUCache<Rectangle2D, Paint> paintCache = new LRUCache<Rectangle2D, Paint>(5);

    protected Gradient()
    {
        super();
    }

    /**
     * Adds a step
     * 
     * @param step the step
     * @return the added step
     */
    protected <STEP_TYPE extends GradientStep> STEP_TYPE add(STEP_TYPE step)
    {
        steps.add(step);

        return step;
    }

    public int getPaintCacheCapacity()
    {
        return paintCache.getCapacity();
    }

    public void setPaintCacheCapacity(int capacity)
    {
        paintCache.setCapacity(capacity);
    }

    /**
     * Creates a {@link LinearGradientPaint} from the specified gradient. If a cache is defined, this method caches the
     * result. It's very wise to make use of this feature. The default cache size is 5.
     * 
     * @param width the width
     * @return the paint
     */
    public Paint toHorizontalPaint(double width)
    {
        return toHorizontalPaint(0, width);
    }

    /**
     * Creates a {@link LinearGradientPaint} from the specified gradient. If a cache is defined, this method caches the
     * result. It's very wise to make use of this feature. The default cache size is 5.
     * 
     * @param x the x position
     * @param width the width
     * @return the paint
     */
    public Paint toHorizontalPaint(double x, double width)
    {
        return toPaint(new Point2D.Double(x, 0), new Point2D.Double(x + width, 0));
    }

    /**
     * Creates a {@link LinearGradientPaint} from the specified gradient. If a cache is defined, this method caches the
     * result. It's very wise to make use of this feature. The default cache size is 5.
     * 
     * @param height the height
     * @return the paint
     */
    public Paint toVerticalPaint(double height)
    {
        return toVerticalPaint(0, height);
    }

    /**
     * Creates a {@link LinearGradientPaint} from the specified gradient. If a cache is defined, this method caches the
     * result. It's very wise to make use of this feature. The default cache size is 5.
     * 
     * @param y the y position
     * @param height the height
     * @return the paint
     */
    public Paint toVerticalPaint(double y, double height)
    {
        return toPaint(new Point2D.Double(0, y), new Point2D.Double(0, y + height));
    }

    /**
     * Creates a {@link LinearGradientPaint} from the specified gradient. If a cache is defined, this method caches the
     * result. It's very wise to make use of this feature. The default cache size is 5.
     * 
     * @param start the start point
     * @param end the end point
     * @return the paint
     */
    public Paint toPaint(Point2D start, Point2D end)
    {
        return toPaint(new Rectangle2D.Double(start.getX(), start.getY(), end.getX() - start.getX(), end.getY()
            - start.getY()));
    }

    /**
     * Creates a {@link LinearGradientPaint} from the specified gradient. If a cache is defined, this method caches the
     * result. It's very wise to make use of this feature. The default cache size is 5.
     * 
     * @param rectangle the rectangle
     * @return the paint
     */
    public Paint toPaint(Rectangle2D rectangle)
    {
        Paint result = paintCache.get(rectangle);

        if (result != null)
        {
            return result;
        }

        Point2D start = new Point2D.Double(rectangle.getX(), rectangle.getY());
        Point2D end =
            new Point2D.Double(rectangle.getX() + rectangle.getWidth(), rectangle.getY() + rectangle.getHeight());
        double distance = start.distance(end);
        int totalStepCount = 0;

        for (int i = 1; i < steps.size(); i += 2)
        {
            totalStepCount += ((GradientLength) steps.get(i)).getStepCount();
        }

        double[] relativeFractions = new double[totalStepCount];
        double[] minimums = new double[totalStepCount];
        double[] maximums = new double[totalStepCount];
        Color[] baseColors = new Color[totalStepCount];
        boolean[] rounds = new boolean[totalStepCount];

        Color startColor = ((GradientColor) steps.get(0)).getColor();
        int stepIndex = 1;
        int pos = 0;

        while (stepIndex < steps.size())
        {
            GradientLength step = (GradientLength) steps.get(stepIndex);
            Color endColor = ((GradientColor) steps.get(stepIndex + 1)).getColor();

            for (int interpolationIndex = 0; interpolationIndex < step.getStepCount(); interpolationIndex += 1)
            {
                relativeFractions[pos] = step.getFraction() / step.getStepCount();
                minimums[pos] =
                    (step.getMinimumLength() <= 0) ? Double.NEGATIVE_INFINITY : step.getMinimumLength() / distance
                        / step.getStepCount();
                maximums[pos] =
                    (step.getMaximumLength() < Integer.MAX_VALUE) ? step.getMaximumLength() / distance
                        / step.getStepCount() : Double.POSITIVE_INFINITY;
                baseColors[pos] =
                    computeColor(startColor, endColor, step.getInterpolation(), (1d / step.getStepCount())
                        * (interpolationIndex + 1));
                rounds[pos] = interpolationIndex == 0;

                pos += 1;
            }

            startColor = endColor;
            stepIndex += 2;
        }

        double[] nearedFractions = Utils.nearFractions(relativeFractions, minimums, maximums);
        double expected = 0;
        double minimum = 0;
        int length = 1;
        double roundingFraction = 1 / distance;
        double minimumFraction = roundingFraction / 10;

        for (int i = 0; i < nearedFractions.length; i += 1)
        {
            length += 1;
            nearedFractions[i] += expected;

            if (rounds[i])
            {
                nearedFractions[i] = Utils.round(nearedFractions[i], roundingFraction);
            }

            expected = nearedFractions[i];

            if ((rounds[i]) && (nearedFractions[i] < (minimum + roundingFraction)))
            {
                nearedFractions[i] = minimum + roundingFraction;
            }
            else if ((!rounds[i]) && (nearedFractions[i] < (minimum + minimumFraction)))
            {
                nearedFractions[i] = minimum + minimumFraction;
            }

            minimum = nearedFractions[i];

            if (nearedFractions[i] >= 1)
            {
                break;
            }
        }

        float[] fractions = new float[length];
        Color[] colors = new Color[length];
        fractions[0] = 0;
        colors[0] = ((GradientColor) steps.get(0)).getColor();

        for (int i = 1; i < fractions.length; i += 1)
        {
            if ((i - 1) >= nearedFractions.length)
            {
                // the last fractions is < 1, repeat the last color
                fractions[i] = 1;
                colors[i] = ((GradientColor) steps.get((i - 1) * 2)).getColor();
            }
            else
            {
                Color color = baseColors[i - 1];
                double endFraction = nearedFractions[i - 1];

                if (endFraction > 1)
                {
                    // the last fraction is > 1, blend the color
                    double startFraction = (i > 1) ? nearedFractions[i - 2] : 0;
                    double factor = (1 - startFraction) / (endFraction - startFraction);
                    fractions[i] = 1;
                    colors[i] = Colors.blendTo(colors[i - 1], color, factor);
                }
                else
                {
                    fractions[i] = (float) endFraction;
                    colors[i] = color;
                }
            }
        }

        result = new LinearGradientPaint(start, end, fractions, colors);

        paintCache.put(rectangle, result);

        return result;
    }

    protected Color computeColor(Color startColor, Color endColor, Interpolation interpolation, double factor)
    {
        factor = Math.min(Math.max(factor, 0), 1);

        if (interpolation != null)
        {
            factor = interpolation.calculate(factor);
        }

        return Colors.blendTo(startColor, endColor, factor);
    }

    public static void main(String[] args)
    {
        Gradient gradient = Gradient.start(RED).fraction(1).end(BLUE);

        gradient.toPaint(new Point2D.Double(0, 0), new Point2D.Double(10, 0));
    }
}
